# FreeBench tool building library
# (c) 2008 Thomas Stromberg <thomas%stromberg.org>

import copy
import urllib
import os
import re
import shutil
import time
import md5
import os.path
import subprocess

# local
import yaml
import sysinfo
import util
import version

class Builder(object):
  def __init__(self, base_dir=None):
    if base_dir:
      self.base_dir = base_dir
    else:
      self.base_dir = os.getcwd()
    self.recipes = None
    self.sysinfo = sysinfo.SysInfo()
    self.compiler = self.FindCompiler()
    self.cxxcompiler = self.FindCxxCompiler()
    self.cflags = os.getenv('CFLAGS', '-O3 -funroll-loops -fomit-frame-pointer -pipe')
    self.build_dir = self.base_dir + '/build'
    self.distfiles_dir = self.base_dir + '/distfiles'
    self.compiled_dir = self.base_dir + '/compiled'
    self.install_dir = self.InstallDirectory()
    for dir in (self.build_dir, self.install_dir + '/bin', self.distfiles_dir, self.install_dir):
      util.mkdir_p(dir)

  def InstallDirectory(self):
    return self.compiled_dir + '/' + self.PlatformString()

  def PlatformString(self):
    machine = self.CompilerArch()
    if not machine:
      return 'no-compiler'

    # Append kernel major version if it is not already there (FreeBSD)
    if not re.search('\d$', machine):
      machine = machine + os.uname()[2].split('.')[0]

    flags = self.cflags.replace(' -pipe', '')
    flags = flags.replace(' -funroll-loops', ' unroll')
    flags = flags.replace(' -fomit-frame-pointer', ' omitfp')
    flags = flags.replace(' ', '')
    flags = flags.replace('-', '')
    return '_'.join((machine, self.CompilerType(), self.CompilerVersion(), flags))


  def FindCompiler(self):
    if os.getenv('CC', None):
      return os.getenv('CC')
    for name in ('icc', 'egcs', 'gcc', 'cc'):
      path = self.FindTool(name)
      if path:
        return path    

  def FindCxxCompiler(self):
    if os.getenv('CXX', None):
      return os.getenv('CXX')
    for name in ('eg++', 'g++', 'c++'):
      path = self.FindTool(name)
      if path:
        return path    

  def CompilerType(self):
    if 'GCC' in self.CompilerBanner():
      return 'gcc'
    else:
      return 'unknown'

  def CompilerBanner(self):
    if self.compiler:
      return os.popen('%s --version | head -1' % self.compiler).read().strip()

  def CompilerVersion(self):
    if self.compiler: 
      return os.popen('%s -dumpversion | head -1' % self.compiler).read().strip()

  def CompilerArch(self):
    if self.compiler:
      return os.popen('%s -dumpmachine | head -1' % self.compiler).read().strip()


  def LoadConfiguration(self):
    """Loads configuration data."""
    return yaml.load(open(self.base_dir + '/recipes.yml').read())

  def Fetch(self, url, filename):
    util.msg("Fetching %s" % url)
    # curl is faster :)
    curl = self.FindTool('curl')
    if curl:
      os.system("%s -f -s -L %s -o %s" % (curl, url, filename))
    else:
      urllib.urlretrieve(url, filename)


  def PackageDirectory(self):
    pkg_pointer_file = self.compiled_dir + "/." + self.PackageName()
    util.debug("Checking for pointer file %s" % pkg_pointer_file)
    
    if not os.path.exists(pkg_pointer_file):
      return None

    f = open(pkg_pointer_file)
    pkg_pointer = f.read().strip()
    util.debug("Package pointer reads %s" % pkg_pointer)
    install_dir = self.compiled_dir + '/' + pkg_pointer
 
    if install_dir and os.path.exists(install_dir):
      return install_dir
    else:
      return None 

  def PackageName(self): 
    return '%s-binaries-%s' % (self.sysinfo.Platform(), version.VERSION)
 
  def FetchPrecompiledBinaries(self):
    """Fetch a tar.gz of precompiled binaries for faster bootstrapping."""
    dist = self.PackageName()
    pkg_dir = self.PackageDirectory()
    if not pkg_dir: 
      util.debug("Did not find a package directory for %s" % dist)
      distfile = dist + '.tar.gz'
      local_file = self.distfiles_dir + '/' + distfile
      if not os.path.exists(local_file):
        url = 'http://sprocket.io/files/freebench/' + distfile
        util.debug("Getting packages from %s" % url)
        try:
          self.Fetch(url, local_file)
        except IOError, e:
          util.msg("Could not download %s: %s" % (url, e))
          return False

        if os.path.exists(local_file) and os.path.getsize(local_file) < 100000:
	  util.debug("Deleting %s, it is too small." % local_file)
          os.unlink(local_file)

        if not os.path.exists(local_file):
          util.msg("No pre-compiled binaries available for %s yet, building from source." % dist)
          return False

      util.msg("Extracting binaries from %s" % local_file)
      try:
        self.Extract(local_file, dir=self.compiled_dir)
      except ValueError, e:
        util.msg("Extraction failure for %s, removing." % local_file)
        os.unlink(local_file)
 
  def Extract(self, filename, dir=None):
    if not dir:
      dir = os.getcwd()
      
    util.debug("Extracting %s to %s" % (filename, dir))
    if '.tar.gz' in filename or '.tgz' in filename:
      extract_cmd = 'gunzip -c %s | tar -xvf -' % filename
    elif '.tar.bz2' in filename or 'tbz' in filename:
      bzip2 = self.FindTool('bzip2')
      if not bzip2:
        (build_dir, bzip2) = self.BuildRecipe('bzip2')
      extract_cmd = '%s -dc %s | tar -xvf -' % (bzip2, filename)
    elif '.tar' in filename:
      extract_cmd = 'tar -xvf -' % filename
    elif '.zip' in filename:
      unzip = self.FindTool('unzip')
      if not unzip:
        (build_dir, unzip) = self.BuildRecipe('unzip')
      extract_cmd = '%s -o %s' % (unzip, filename)

    # This is useful because building recipes moves us!      
    util.chdir(dir)
    if extract_cmd:
      (code, stdout, stderr) = self.Exec(extract_cmd)
      if code:
        raise ValueError("'%s' exited with code %s: %s" % (extract_cmd, code, stderr))

  def BuildRecipe(self, recipe_name):
    """Builds all of the dependencies for a recipe.
    
    Returns:
      (directory, last_path)
    """
    resource = None
    pkg_dir = None
    use_packages = True
    
    if not self.recipes:
      self.recipes = self.LoadConfiguration()
      pkg_dir = self.PackageDirectory()
       
    recipe = self.recipes[recipe_name]
    # First try precompiled packages if we can meet all dependencies.
    if pkg_dir:
      for target in recipe['targets']:
        pkg_path = pkg_dir + '/' + target['resource']
        if not os.path.exists(pkg_path):
          util.debug("%s does not exist" % pkg_path)
          use_packages = False
          break
    
      if use_packages:
        util.debug("We are able to rely on packages for %s" % recipe_name)
        return (pkg_dir, pkg_path)
 
    for target in recipe['targets']:
      path = self.install_dir + '/' + target['resource']
      if not os.path.exists(path):
        self.BuildTarget(target)
        if not os.path.exists(path):
          raise ValueError('%s is still missing.' % path)
    # We only care about the last one
    util.debug('%s = %s' % (recipe_name, path))
    return (self.install_dir, path)

  def BuildTarget(self, base_target):
    if not self.compiler:
      raise ValueError("Could not find a compiler to compile '%s'" % base_target['resource'])
    
    util.msg("Compiling '%s'" % base_target['resource'])
    target = copy.copy(base_target)
    if self.sysinfo.os in base_target:
      util.debug("Merging specific build rules for %s" % self.sysinfo.os)
      target.update(base_target[self.sysinfo.os])
    
    cmd = None
    base_filename = target['source'].split('/')[-1]
    filename = self.distfiles_dir + '/' + target['source'].split('/')[-1]
    if not os.path.exists(filename):
      self.Fetch(target['source'], filename)

    if md5.new(open(filename).read()).hexdigest() != target['md5']:
      util.msg("%s has bad checksum, refetching" % filename)
      self.Fetch(target['source'], filename)
      if md5.new(open(filename).read()).hexdigest() != target['md5']:
        raise ValueError('Bad MD5 hash for %s' % filename)

    self.Extract(filename, dir=self.build_dir)
    subdir = target.get('build_dir', '.'.join(base_filename.split('.')[0:-2]))
    directory =  self.build_dir + '/' + subdir

    if target.get('gnu_make', None):
      make = self.FindTool('gmake')
      if not make:
        make = self.FindTool('make', version_required='GNU Make')
        if not make:
          (dir, make) = self.BuildRecipe('gmake')
    else:
      make = self.FindTool('make')
      
    if not make:
      raise ValueError("Could not find 'make' tool to build with. Please install!")
    cflags = copy.copy(self.cflags)
    if 'gcc_max_optimization' in target:
      match = re.search('-(O\d*)', cflags)
      if match:
        opt_level = match.groups()[0]
        if opt_level > target['gcc_max_optimization']:
          cflags = re.sub('-O\d+', '-%s' % target['gcc_max_optimization'], cflags)

    os.putenv('CC', self.compiler)
    os.putenv('CXX', self.cxxcompiler)
    os.putenv('CFLAGS', cflags)
    os.putenv('CXXFLAGS', cflags)
    util.debug("C: %s %s" % (self.compiler, cflags))
    util.debug("C++: %s %s" % (self.cxxcompiler, cflags))

    # TODO(thomas): Reset environment when we are done.
    if 'env' in target:
      for pair in target['env'].split(' '):
        (key, value) = pair.split('=')
        util.debug("setenv %s=%s" % (key, value))
        os.putenv(key, value)

    util.debug("Changing to build directory for %s in %s" % (target['resource'], directory))
    util.chdir(directory)
    if 'configure' in target:
      cmd = target['configure']
    elif os.path.exists('configure'):
      cmd = './configure --prefix=%s' % self.install_dir
    if cmd:
      (code, stdout, stderr) = self.Exec(cmd)
      if code:
        raise ValueError("'%s' returned with exit code %s" % (cmd, code))

    if 'clean' in target:
      self.Exec("%s clean 2>/dev/null" % make)
    make_cmd = target.get('make', make)
    install_cmd = target.get('install', '%s install PREFIX=INSTALL_DIR' % make)

    for cmd in (make_cmd, install_cmd):
      (code, stdout, stderr) = self.Exec(cmd)
      if code:
        util.msg("'%s' returned with exit code %s" % (cmd, code))
        
    return True

  def FindTool(self, name, version_required=None):
    dirs = os.environ['PATH'].split(':')
    if hasattr(self, 'install_dir'): 
      dirs.append('%s/bin' % self.install_dir)
    for directory in dirs:
      bin_path = '%s/%s' % (directory, name)
      if os.path.exists(bin_path):
        if version_required:
          (code, stdout, stderr) = self.Exec('%s -v 2>/dev/null' % bin_path)
          if version_required in stdout:
            util.debug('Found %s matching %s: %s' % (name, version_required, bin_path))
            return bin_path
          else:
            util.debug("'%s' not found in: %s" % (version_required, stdout))
        else:
          util.debug("Found %s" % bin_path)
          return bin_path
    return None

  def _ExpandMacros(self, string):
    if not string:
      util.debug("Cannot expand an empty string, duh.")
      return string
  
    for macro in ('DISTFILES_DIR', 'BUILD_DIR', 'INSTALL_DIR',
                  'CFLAGS', 'COMPILER'):
      if macro in string:
        string = string.replace(macro, str(getattr(self, macro.lower())))
    return string

  def Exec(self, raw_command):
    # This will break horribly if spaces are used anywhere.
    command = self._ExpandMacros(raw_command)
    proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE, close_fds=True)
    util.debug("  > %s: '%s'" % (proc.pid, command))
    stdout, stderr = proc.communicate()
    code = proc.wait()
    if code:
      util.msg('exit %s for %s: out=%s err=%s' % (code, command, stdout, stderr))
    return (code, stdout, stderr)

    
    
    
